﻿using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Security.Claims;
using System.Threading.Tasks;
using Flickr.Owin.Security.Constants;
using Flickr.Owin.Security.FlickrOAuth;
using Microsoft.Owin;
using Microsoft.Owin.Infrastructure;
using Microsoft.Owin.Logging;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Infrastructure;

namespace Flickr.Owin.Security
{
    internal class FlickrAuthenticationHandler : AuthenticationHandler<FlickrAuthenticationOptions>
    {
        private readonly ILogger _logger;

        private readonly HttpClient _httpClient;

        public FlickrAuthenticationHandler(HttpClient httpClient, ILogger logger)
        {
            _httpClient = httpClient;
            _logger = logger;
        }

        protected override async Task<AuthenticationTicket> AuthenticateCoreAsync()
        {
            AuthenticationProperties properties = null;

            try
            {
                IReadableStringCollection query = Request.Query;

                properties = UnpackStateParameter(query);

                if (properties == null)
                {
                    _logger.WriteWarning("Invalid return state");
                    return null;
                }

                // Anti-CSRF
                if (!ValidateCorrelationId(properties, _logger))
                {
                    return new AuthenticationTicket(null, properties);
                }

                var oauthVerifier = Request.Query[FlickrAuthenticationConstants.OauthVerifier];

                if (string.IsNullOrEmpty(oauthVerifier))
                {
                    _logger.WriteWarning("Missing oauth verifier");
                    return new AuthenticationTicket(null, properties);
                }
                
                var requestTokenProperties = GetRequestTokenProperties();

                string requestToken;
                
                requestTokenProperties.Dictionary.TryGetValue(FlickrAuthenticationConstants.RequestToken, out requestToken);

                if (string.IsNullOrEmpty(requestToken))
                {
                    _logger.WriteWarning("Missing request token");
                    return new AuthenticationTicket(null, properties);
                }

                string requestTokenSecret;

                requestTokenProperties.Dictionary.TryGetValue(FlickrAuthenticationConstants.RequestTokenSecret, out requestTokenSecret);

                if (string.IsNullOrEmpty(requestTokenSecret))
                {
                    _logger.WriteWarning("Missing request token Secret");
                    return new AuthenticationTicket(null, properties);
                }

                OAuthAccessToken accessToken = await FlickrOAuthHandler.GetAccessToken(requestToken,
                                                                                       requestTokenSecret,
                                                                                       oauthVerifier,
                                                                                       this.Options.AppKey,
                                                                                       this.Options.AppSecret,
                                                                                       this._httpClient,
                                                                                       this.Request);
                if (accessToken == null)
                {
                    _logger.WriteWarning("access denied");
                    return new AuthenticationTicket(null, properties);
                }

                ClaimsIdentity identity = new ClaimsIdentity(this.Options.AuthenticationType);

                identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, accessToken.UserId, null, "Flickr"));
                identity.AddClaim(new Claim(ClaimTypes.Name, accessToken.Username));
                identity.AddClaim(new Claim(FlickrClaimsTypes.AccessToken, accessToken.Token));
                identity.AddClaim(new Claim(FlickrClaimsTypes.AccessTokenSecret, accessToken.TokenSecret));
                identity.AddClaim(new Claim(FlickrClaimsTypes.FullName, accessToken.FullName));

                var context = new FlickrAuthenticatedContext(
                      Context,
                      identity,
                      properties
                      );

                await Options.Provider.Authenticated(context);

                return new AuthenticationTicket(context.Identity, context.Properties);
            }
            catch (Exception ex)
            {
                _logger.WriteError("Authentication failed", ex);
                return new AuthenticationTicket(null, properties);
            }
        }

        public override async Task<bool> InvokeAsync()
        {
            bool flag;
            if (this.Options.CallbackPath.HasValue && this.Options.CallbackPath == this.Request.Path)
                flag = await this.InvokeReturnPathAsync();
            else
                flag = false;
            return flag;
        }

        protected override async Task ApplyResponseChallengeAsync()
        {
            if (Response.StatusCode == 401)
            {
                AuthenticationResponseChallenge responseChallenge =
                    this.Helper.LookupChallenge(this.Options.AuthenticationType, this.Options.AuthenticationMode);
                if (responseChallenge != null)
                {
                    string stringToEscape = this.Request.Scheme + (object) Uri.SchemeDelimiter + this.Request.Host.Value;

                    AuthenticationProperties properties = responseChallenge.Properties;

                    if (string.IsNullOrEmpty(properties.RedirectUri))
                    {
                        properties.RedirectUri = string.Concat(new object[4]
                        {
                            stringToEscape,
                            this.Request.PathBase,
                            this.Request.Path,
                            this.Request.QueryString
                        });
                    }

                    // Anti-CSRF
                    GenerateCorrelationId(properties);

                    string redirectUri = this.BuildReturnTo(this.Options.StateDataFormat.Protect(properties));

                    var requestToken = await FlickrOAuthHandler.GetRequestToken(redirectUri, this.Options.AppKey,
                        this.Options.AppSecret, this._httpClient, this.Request);

                    if (requestToken.CallbackConfirmed)
                    {
                        properties.Dictionary.Add(FlickrAuthenticationConstants.RequestToken, requestToken.Token);
                        properties.Dictionary.Add(FlickrAuthenticationConstants.RequestTokenSecret,
                            requestToken.TokenSecret);

                        GenerateRequestTokenCookies(properties);

                        redirectUri = FlickrOAuthHandler.OAuthCalculateAuthorizationUrl(requestToken.Token,
                            this.Options.AuthLevel);

                        this.Options.Provider.ApplyRedirect(new FlickrApplyRedirectContext(this.Context, this.Options,
                            properties, redirectUri));
                    }
                    else
                    {
                        this._logger.WriteError("requestToken CallbackConfirmed!=true");
                    }
                }
            }
        }


        private void GenerateRequestTokenCookies(AuthenticationProperties properties)
        {
            if (properties == null)
            {
                throw new ArgumentNullException("properties");
            }

            var cookieOptions = new CookieOptions
            {
                HttpOnly = true,
                Secure = Request.IsSecure
            };

            string token;

            string tokenSecret;

            properties.Dictionary.TryGetValue(FlickrAuthenticationConstants.RequestToken, out token);

            properties.Dictionary.TryGetValue(FlickrAuthenticationConstants.RequestTokenSecret, out tokenSecret);

            if (!string.IsNullOrEmpty(token) && !string.IsNullOrEmpty(tokenSecret))
            {
                var requestTokenProperties = new AuthenticationProperties();

                requestTokenProperties.Dictionary.Add(FlickrAuthenticationConstants.RequestToken, token);
                requestTokenProperties.Dictionary.Add(FlickrAuthenticationConstants.RequestTokenSecret, tokenSecret);

                var protectedToken = this.Options.StateDataFormat.Protect(requestTokenProperties);

                Response.Cookies.Append(FlickrAuthenticationConstants.RequestToken, protectedToken, cookieOptions);
            }
        }


        private AuthenticationProperties GetRequestTokenProperties()
        {
            string requestTokenCookie = Request.Cookies[FlickrAuthenticationConstants.RequestToken];
            
            if (string.IsNullOrWhiteSpace(requestTokenCookie))
            {
                _logger.WriteWarning("{0} cookie not found.", FlickrAuthenticationConstants.RequestToken);
            }

            Response.Cookies.Delete(FlickrAuthenticationConstants.RequestToken);

            var properties = this.Options.StateDataFormat.Unprotect(requestTokenCookie);

            return properties;
        }

        private static string GetStateParameter(IReadableStringCollection query)
        {
            IList<string> values = query.GetValues("state");
            if (values != null && values.Count == 1)
                return values[0];
            else
                return (string)null;
        }

        private AuthenticationProperties UnpackStateParameter(IReadableStringCollection query)
        {
            string stateParameter = FlickrAuthenticationHandler.GetStateParameter(query);
            if (stateParameter != null)
                return this.Options.StateDataFormat.Unprotect(stateParameter);
            else
                return (AuthenticationProperties)null;
        }

        private string BuildReturnTo(string state)
        {
            return this.Request.Scheme + (object)"://" + this.Request.Host + this.RequestPathBase + this.Options.CallbackPath + "?state=" + Uri.EscapeDataString(state);
        }

        public async Task<bool> InvokeReturnPathAsync()
        {
            AuthenticationTicket model = await AuthenticateAsync();
            if (model == null)
            {
                _logger.WriteWarning("Invalid return state, unable to redirect.");
                Response.StatusCode = 500;
                return true;
            }

            var context = new FlickrReturnEndpointContext(Context, model);
            context.SignInAsAuthenticationType = Options.SignInAsAuthenticationType;
            context.RedirectUri = model.Properties.RedirectUri;
            model.Properties.RedirectUri = null;

            await Options.Provider.ReturnEndpoint(context);

            if (context.SignInAsAuthenticationType != null && context.Identity != null)
            {
                ClaimsIdentity signInIdentity = context.Identity;
                if (!string.Equals(signInIdentity.AuthenticationType, context.SignInAsAuthenticationType, StringComparison.Ordinal))
                {
                    signInIdentity = new ClaimsIdentity(signInIdentity.Claims, context.SignInAsAuthenticationType, signInIdentity.NameClaimType, signInIdentity.RoleClaimType);
                }
                Context.Authentication.SignIn(context.Properties, signInIdentity);
            }

            if (!context.IsRequestCompleted && context.RedirectUri != null)
            {
                if (context.Identity == null)
                {
                    // add a redirect hint that sign-in failed in some way
                    context.RedirectUri = WebUtilities.AddQueryString(context.RedirectUri, "error", "access_denied");
                }
                Response.Redirect(context.RedirectUri);
                context.RequestCompleted();
            }

            return context.IsRequestCompleted;
        }
    }
}